package cn.sc.summer.mybatis.config.interceptor;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.sc.summer.constant.project.ProjectConstant;
import cn.sc.summer.exception.exceptions.CommonException;
import cn.sc.summer.mybatis.builder.SqlBuilderDirector;
import cn.sc.summer.mybatis.builder.SqlInfoBuilder;
import cn.sc.summer.mybatis.event.SqlEvent;
import cn.sc.summer.mybatis.po.Sql;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser;
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlSchemaStatVisitor;
import com.alibaba.druid.sql.parser.SQLStatementParser;
import com.alibaba.druid.stat.TableStat;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.context.ApplicationContext;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.sql.Statement;
import java.util.*;
import java.util.stream.Collectors;

/**
 * mybatis sql 日志拦截器, 用于打印SQL相关信息（例如：实际执行SQL语句，SQL执行时间，查询记录数等）
 * 关于 @Intercepts注解说明：
 * 为了让拦截器能够精确地拦截特定的方法，需要使用 @Intercepts 注解来声明拦截的方法和参数类型
 * 该注解包含一个参数，即一个 @Signature 类型的数组，用于指定要拦截的方法。每个 @Signature 注解表示一个要拦截的方法签名，其中包括以下属性：
 * type：被拦截的目标类或接口。在这里，StatementHandler.class 表示拦截 MyBatis 中的 StatementHandler 类。
 * method：被拦截的方法名。可以通过字符串指定方法名或使用方法引用。
 * args：被拦截方法的参数类型数组。用于指定被拦截方法的参数类型及顺序。
 * 在以下代码中的注解示例中，拦截器指定了对 StatementHandler 类中的三个方法进行拦截，分别是：
 * query(Statement.class, ResultHandler.class)：拦截 StatementHandler 类中的 query 方法，该方法有两个参数，分别是 Statement 和 ResultHandler。
 * update(Statement.class)：拦截 StatementHandler 类中的 update 方法，该方法有一个参数，即 Statement。
 * batch(Statement.class)：拦截 StatementHandler 类中的 batch 方法，该方法也有一个参数，即 Statement。
 * 通过使用 @Intercepts 注解和 @Signature 注解，可以精确地指定要拦截的方法和参数类型，从而实现对特定方法的拦截和处理。
 *
 * @author a-xin
 */
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
public class SqlLog implements Interceptor {

    /**
     * 定义一个包含需要添加单引号括起来的参数类型集合。
     */
    private static final Set<String> NEED_BRACKETS = Collections.unmodifiableSet(new HashSet<String>() {
        private static final long serialVersionUID = 1L;

        {
            add("String");
            add("Date");
            add("Time");
            add("LocalTime");
            add("LocalDateTime");
            add("BigDecimal");
            add("Timestamp");
            add("LocalDate");
        }
    });

    /**
     * 打印sql忽略表
     */
    public static final List<String> IGNORE_TABLE_LIST = Collections.unmodifiableList(new ArrayList<String>() {
        private static final long serialVersionUID = 1L;

        {
            add("sys_log");
            add("sys_sql");
            add("sys_system_info");
            add("gateway_log");
            add("sys_task_log");
            add("sys_task");
            add("sys_api");
        }
    });

    /**
     * MyBatis的配置对象。
     */
    private Configuration configuration = null;

    /**
     * 使用监听者设计模式对sql进行监听并发送至消息队列
     */
    @Resource
    private ApplicationContext applicationContext;

    /**
     * 执行sql信息map
     */
    private final Map<String, String> SQL_INFO_MAP = new HashMap<>();

    /**
     * 执行原始sql
     */
    private final static String ORIGINAL_SQL = "ORIGINAL_SQL";

    /**
     * 执行sql参数信息
     */
    private final static String SQL_PARAMS = "SQL_PARAMS";

    /**
     * 慢sql执行上线时间，毫秒
     */
    private static Long SQL_TIME;

    @PostConstruct
    public static void init(Long sqlTime) {
        SqlLog.SQL_TIME = sqlTime;
    }

    /**
     * 拦截器的核心方法，用于拦截并处理SQL语句执行前后的逻辑。
     *
     * @param invocation Invocation 类是 MyBatis 框架提供的一个接口，定义了用于描述方法执行的信息和操作的方法。
     *                   在方法拦截时，MyBatis 将被拦截的方法封装成 Invocation 对象，并作为参数传递给拦截器。
     *                   Invocation 接口中定义了以下几个重要的方法：
     *                   Object getTarget()：获取目标对象，即被拦截的对象。
     *                   Method getMethod()：获取被拦截的方法对象。
     *                   Object[] getArgs()：获取被拦截方法的参数列表。
     *                   Object proceed() throws Throwable：继续执行被拦截的方法。
     *                   Object proceed(Object[] args) throws Throwable：继续执行被拦截的方法，并使用指定的参数列表。
     *                   Object getThis()：获取代理对象，即拦截器生成的代理对象。
     *                   通过 Invocation 对象，我们可以获取被拦截方法的相关信息，如目标对象、方法名称、参数列表等。拦截器可以根据这些信息对方法进行额外的处理，比如记录日志、性能监控、权限验证等。最后，通过调用 proceed() 方法，可以继续执行被拦截的方法。
     *                   在代码中，Invocation invocation 参数被用于执行被拦截方法，并在方法执行前后进行一些额外的操作。
     * @return 流程继续执行
     * @throws Throwable 异常抛出
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        long startTime = System.currentTimeMillis();
        long sqlCost = 0;
        int lines = 0;
        Boolean status = Boolean.FALSE;
        try {
            Object proceed = invocation.proceed();
            sqlCost = System.currentTimeMillis() - startTime;
            lines = ObjectUtil.isEmpty(proceed) ? 0 : 1;
            if (proceed instanceof Collection<?>) {
                lines = ((Collection<?>) proceed).size();
            }
            status = Boolean.TRUE;
            return proceed;
        } finally {
            String executeSQL = this.getSql(target);
            printlnSql(executeSQL, sqlCost, lines, status);
        }
    }

    /**
     * 打印sql执行日志信息
     *
     * @param executeSQL 执行sql
     * @param sqlCost    sql执行花费时间
     * @param lines      返回数据条数
     * @param status     是否执行成功
     */
    private void printlnSql(String executeSQL, Long sqlCost, Integer lines, Boolean status) {
        if (StrUtil.isNotBlank(executeSQL)) {
            log.debug("==>> SQL Information：{}", executeSQL);
            if (sqlCost > SQL_TIME) {
                log.error("==>> SQL time : {} mm, Slow SQL ：{}", SQL_TIME, executeSQL);
                Sql sqlInfo = new SqlBuilderDirector(new SqlInfoBuilder()).build();
                if (StrUtil.isNotBlank(sqlInfo.getId())) {
                    sqlInfo.setOriginalTime(SQL_TIME);
                    sqlInfo.setApiServiceName(ProjectConstant.SERVER_NAME);
                    sqlInfo.setOriginalSql(SQL_INFO_MAP.getOrDefault(ORIGINAL_SQL, "null"));
                    sqlInfo.setParams(SQL_INFO_MAP.getOrDefault(SQL_PARAMS, "This method has no parameters!"));
                    sqlInfo.setTime(sqlCost);
                    sqlInfo.setExecuteSql(executeSQL);
                    applicationContext.publishEvent(new SqlEvent(sqlInfo));
                }
            }
            if (status) {
                log.debug("<<== SQL Execution results：success. Take：{} mm. Total result：{}", sqlCost, lines);
            } else {
                log.error("<<== SQL Execution results：failed. Take：{} mm. Total result：{}", sqlCost, lines);
            }
        }
    }

    /**
     * 获取 SQL 语句
     *
     * @param target 获取目标对象，即被拦截的对象。
     * @return 实际执行SQL语句
     */
    private String getSql(Object target) {
        try {
            // 获取 StatementHandler 对象
            StatementHandler statementHandler = (StatementHandler) target;
            // 获取 BoundSql 对象
            BoundSql boundSql = statementHandler.getBoundSql();
            if (configuration == null) {
                // 通过反射获取 Configuration 对象
                final ParameterHandler parameterHandler = statementHandler.getParameterHandler();
                this.configuration = (Configuration) FieldUtils.readField(parameterHandler, "configuration", true);
            }
            // 格式化 SQL 语句并返回
            return formatSql(boundSql, configuration);
        } catch (Exception e) {
            // 异常处理，打印警告日志
            log.warn("An SQL statement that could not be resolved：{}", target, e);
            throw new CommonException(e.getMessage(), e);
        }
    }

    /**
     * 格式化 SQL 语句
     *
     * @param boundSql      绑定的 SQL 对象，包含 SQL 语句和参数信息
     * @param configuration MyBatis 的配置信息对象，用于获取配置信息
     * @return 格式化后的 SQL 字符串
     */
    private String formatSql(BoundSql boundSql, Configuration configuration) {
        // 获取原始 SQL 语句
        String sql = boundSql.getSql();

        List<String> allTableNameBySQL = getAllTableNameBySQL(sql);
        if (CollUtil.isNotEmpty(allTableNameBySQL) && allTableNameBySQL.size() == 1 && IGNORE_TABLE_LIST.contains(allTableNameBySQL.get(0))) {
            return null;
        }
        log.debug("==>> The Raw SQL: {}", sql);
        SQL_INFO_MAP.put(ORIGINAL_SQL, sql);

        // 获取参数对象
        Object parameterObject = boundSql.getParameterObject();
        // 判断是否为空
        if (StringUtils.isEmpty(sql) || Objects.isNull(configuration)) {
            return null;
        }

        // 获取 TypeHandlerRegistry 对象
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        // 移除 SQL 字符串中的空格、换行符等
        sql = sql.replaceAll("[\n\r ]+", " ");
        // 获取参数映射列表
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        List<ParameterMapping> parameterMappingList = parameterMappings.stream()
                .filter(it -> it.getMode() != ParameterMode.OUT)
                .collect(Collectors.toList());

        // 使用 StringBuilder 保存格式化后的 SQL
        final StringBuilder result = new StringBuilder(sql);

        List<String> params = new ArrayList<>();
        // 解析问号并替换参数
        for (int i = result.length(); i > 0; i--) {
            if (result.charAt(i - 1) != '?') {
                continue;
            }
            ParameterMapping parameterMapping = parameterMappingList.get(parameterMappingList.size() - 1);
            Object value;
            String propertyName = parameterMapping.getProperty();
            // 判断绑定的附加参数中是否有对应的属性名
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                // 使用 MetaObject 获取属性值
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            String type = "";
            if (value != null) {
                // 判断参数类型，如果是需要添加括号的类型，则添加单引号
                type = value.getClass().getSimpleName();
                if (NEED_BRACKETS.contains(type)) {
                    result.replace(i - 1, i, "'" + value + "'");
                } else {
                    result.replace(i - 1, i, value.toString());
                }
            } else {
                // 参数值为空时，替换为 "null"
                result.replace(i - 1, i, "null");
            }
            params.add(value + "(" + type + ")");
            // 移除已处理的参数映射
            parameterMappingList.remove(parameterMappingList.size() - 1);
        }
        if (CollUtil.isNotEmpty(params)) {
            log.debug("==>> The SQL params: {}", String.join(",", params));
            SQL_INFO_MAP.put(SQL_PARAMS, String.join(",", params));
        } else {
            log.debug("==>> This method has no parameters!");
        }

        return result.toString();
    }


    /**
     * 根据sql解析sql中包含的表名称
     *
     * @param sql 执行sql语句
     * @return 包含的表名
     */
    private static List<String> getAllTableNameBySQL(String sql) {
        SQLStatementParser parser = new MySqlStatementParser(sql);
        // 使用Parser解析生成AST，这里SQLStatement就是AST
        SQLStatement sqlStatement = parser.parseStatement();
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
        sqlStatement.accept(visitor);
        Map<TableStat.Name, TableStat> tables = visitor.getTables();
        List<String> allTableName = new ArrayList<>();
        for (TableStat.Name t : tables.keySet()) {
            allTableName.add(t.getName());
        }
        return allTableName;
    }


}
